// Copyright (c) 2019 Presto Labs Pte. Ltd.
// Author: jaewon

#pragma once

#include <experimental/filesystem>
#include <map>
#include <sstream>
#include <string>
#include <utility>

#include <boost/preprocessor/stringize.hpp>
#include <fmt/format.h>
#include <glog/logging.h>
#include <spdlog/sinks/basic_file_sink.h>
#include <spdlog/spdlog.h>

#include "coin2/app/log.pb.h"

// Utility macros for showing informative exception messages including stacktrace.

namespace coin2::base {

static bool HasLoggedMessageSince(
    const char* file_name,
    int line_number,
    int64_t past_nanoseconds,
    int64_t timestamp) {
  int64_t when = timestamp - past_nanoseconds;
  static std::map<std::pair<std::string, int>, int64_t> logged_messages;
  static std::map<const char*, std::string> cached_filenames;
  if (cached_filenames.count(file_name) == 0) {
    std::stringstream str;
    str << file_name;
    cached_filenames[file_name] = str.str();
  }
  const std::string& key = cached_filenames.at(file_name);
  const auto& insert_pair =
      logged_messages.insert(std::make_pair(std::make_pair(key, line_number), timestamp));
  if (insert_pair.second) {
    return false;
  } else {
    if (insert_pair.first->second >= when) {
      return true;
    }
    insert_pair.first->second = timestamp;
    return false;
  }
  (void)HasLoggedMessageSince;
}

// no wait if failed to get mutex, ignored write log once
static bool HasLoggedMessageSinceWeakThreadSafe(
    const char* file_name,
    int line_number,
    int64_t past_nanoseconds,
    int64_t timestamp) {
  static std::map<std::string, int64_t> logged_messages;
  static std::atomic<bool> in_use{false};
  bool unused = false;
  bool inused = true;

  int64_t when = timestamp - past_nanoseconds;
  std::stringstream str;
  str << file_name << ":" << line_number;
  const std::string key = str.str();
  if (!in_use.compare_exchange_strong(unused, inused)) {
    return true;  // do not log
  }
  const auto& insert_pair = logged_messages.insert(std::make_pair(key, timestamp));
  bool ret = false;
  if (!insert_pair.second) {
    if (insert_pair.first->second >= when) {
      ret = true;
    } else {
      insert_pair.first->second = timestamp;
      ret = false;
    }
  }
  in_use.compare_exchange_strong(inused, unused);
  (void)HasLoggedMessageSinceWeakThreadSafe;

  return ret;
}

// wait until get mutex, ensure write log
static bool HasLoggedMessageSinceStrongThreadSafe(
    const char* file_name,
    int line_number,
    int64_t past_nanoseconds,
    int64_t timestamp) {
  static std::map<std::string, int64_t> logged_messages;
  static std::atomic<bool> in_use{false};

  int64_t when = timestamp - past_nanoseconds;
  std::stringstream str;
  str << file_name << ":" << line_number;
  const std::string key = str.str();
  bool ret = false;
  while (true) {
    bool unused = false;
    bool inused = true;
    if (!in_use.compare_exchange_strong(unused, inused)) {
      // std::this_thread::sleep_for(std::chrono::nanoseconds(10));
      continue;
    }
    const auto& insert_pair = logged_messages.insert(std::make_pair(key, timestamp));
    if (!insert_pair.second) {
      if (insert_pair.first->second >= when) {
        ret = true;
      } else {
        insert_pair.first->second = timestamp;
        ret = false;
      }
    }
    in_use.compare_exchange_strong(inused, unused);
    break;
  }
  (void)HasLoggedMessageSinceStrongThreadSafe;

  return ret;
}

}  // namespace coin2::base

#define LOG_EVERY_NS(log_level, past_nanoseconds, timestamp) \
  coin2::base::HasLoggedMessageSinceWeakThreadSafe(          \
      __FILE__,                                              \
      __LINE__,                                              \
      past_nanoseconds,                                      \
      timestamp)                                             \
      ? void()                                               \
      : google::LogMessageVoidify() & LOG(log_level)

#define SPDLOG_INFO_EVERY_NS(past_nanoseconds, timestamp, ...)                         \
  !coin2::base::HasLoggedMessageSince(__FILE__, __LINE__, past_nanoseconds, timestamp) \
      ? SPDLOG_INFO(__VA_ARGS__)                                                       \
      : void()

#define SPDLOG_WARN_EVERY_NS(past_nanoseconds, timestamp, ...)                         \
  !coin2::base::HasLoggedMessageSince(__FILE__, __LINE__, past_nanoseconds, timestamp) \
      ? SPDLOG_WARN(__VA_ARGS__)                                                       \
      : void()

namespace coin2::base {

void InitLogging(const char* app_name, const ::coin2::app::LogConfig& config);

inline bool LogEveryDuration(int64_t* log_time, int64_t timestamp, int64_t cutoff) {
  if (*log_time + cutoff < timestamp) {
    *log_time = timestamp;
    return true;
  }
  return false;
}
}  // namespace coin2::base

namespace impl {

class ExceptionThrower {
 public:
  ~ExceptionThrower() noexcept(false) {
    // Throwing an exception is regarded as bad practice. Howevenr, this class will only be
    // used as nameless temporaries, considered to be the only allowed case.
    throw std::runtime_error(msg_.str());
  }

  std::ostream& stream() { return msg_; }

 private:
  std::stringstream msg_{};
};

}  // namespace impl

#define THROW() ::impl::ExceptionThrower().stream()

#define CHECK_THROW_INTERNAL(condition) \
  if (__builtin_expect(!(condition), 0)) THROW() << "Check failed: "

#if defined(CHECK_THROW)
// Defined at folly exception but nobody will use it.
// https://github.com/facebook/folly/blob/master/folly/Exception.h
#undef CHECK_THROW
#endif
#define CHECK_THROW(condition) CHECK_THROW_INTERNAL(condition) BOOST_PP_STRINGIZE(condition) " "
#define CHECK_NOT_THROW(condition) CHECK_THROW(!(condition))

#define CHECK_OP_THROW(op, val1, val2)         \
  CHECK_THROW_INTERNAL((val1)op(val2))         \
  BOOST_PP_STRINGIZE(val1)                     \
  " " BOOST_PP_STRINGIZE(op) " " BOOST_PP_STRINGIZE(val2) " ("                               \
      << ::google::GetReferenceableValue(val1) \
      << " vs. "                               \
      << ::google::GetReferenceableValue(val2) \
      << ") "

#define CHECK_EQ_THROW(val1, val2) CHECK_OP_THROW(==, val1, val2)
#define CHECK_NE_THROW(val1, val2) CHECK_OP_THROW(!=, val1, val2)
#define CHECK_LE_THROW(val1, val2) CHECK_OP_THROW(<=, val1, val2)
#define CHECK_LT_THROW(val1, val2) CHECK_OP_THROW(<, val1, val2)
#define CHECK_GE_THROW(val1, val2) CHECK_OP_THROW(>=, val1, val2)
#define CHECK_GT_THROW(val1, val2) CHECK_OP_THROW(>, val1, val2)

#if DCHECK_IS_ON()

#define DCHECK_THROW(condition) CHECK_THROW(condition)
#define DCHECK_NOT_THROW(condition) CHECK_NOT_THROW(condition)
#define DCHECK_EQ_THROW(val1, val2) CHECK_EQ_THROW(val1, val2)
#define DCHECK_NE_THROW(val1, val2) CHECK_NE_THROW(val1, val2)
#define DCHECK_LE_THROW(val1, val2) CHECK_LE_THROW(val1, val2)
#define DCHECK_LT_THROW(val1, val2) CHECK_LT_THROW(val1, val2)
#define DCHECK_GE_THROW(val1, val2) CHECK_GE_THROW(val1, val2)
#define DCHECK_GT_THROW(val1, val2) CHECK_GT_THROW(val1, val2)

#else  //! DCHECK_IS_ON()

#define DCHECK_THROW(condition, exception) \
  while (false) CHECK_THROW(condition, exception)
#define DCHECK_NOT_THROW(condition, exception) \
  while (false) CHECK_NOT_THROW(condition, exception)
#define DCHECK_EQ_THROW(val1, val2, exception) \
  while (false) CHECK_EQ_THROW(val1, val2, exception)
#define DCHECK_NE_THROW(val1, val2, exception) \
  while (false) CHECK_NE_THROW(val1, val2, exception)
#define DCHECK_LE_THROW(val1, val2, exception) \
  while (false) CHECK_LE_THROW(val1, val2, exception)
#define DCHECK_LT_THROW(val1, val2, exception) \
  while (false) CHECK_LT_THROW(val1, val2, exception)
#define DCHECK_GE_THROW(val1, val2, exception) \
  while (false) CHECK_GE_THROW(val1, val2, exception)
#define DCHECK_GT_THROW(val1, val2, exception) \
  while (false) CHECK_GT_THROW(val1, val2, exception)

#endif  // DCHECK_IS_ON()
